package com.tc.data.datasource.interceptor;

import com.tc.data.common.annotation.RepositorySharding;
import com.tc.data.common.holder.StrategyHolder;
import com.tc.data.common.strategy.repository.NoRepositoryShardingStrategy;
import com.tc.data.common.strategy.repository.RepositoryShardingStrategy;
import com.tc.data.common.util.CleanType;
import com.tc.data.common.util.Utils;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 分库拦截器
 * 
 * @author Anders Zhu
 * 
 */
public class RepositoryShardingDataSourceInterceptor
		implements MethodInterceptor, InitializingBean, ApplicationContextAware {

	private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryShardingDataSourceInterceptor.class);

	private Map<String, RepositoryShardingStrategy> repositoryShardingStrategyMap = new HashMap<String, RepositoryShardingStrategy>();
	private ApplicationContext applicationContext;
	private ParameterNameDiscoverer paraNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		String debugInfo = "[" + invocation.toString() + "]";

		LOGGER.debug("enter into repository sharding data source interceptor {}", debugInfo);

		// 注解准备
		Method realMethod = MethodHelper.getRealMethod(invocation);
		RepositorySharding rsAnno = realMethod.getAnnotation(RepositorySharding.class);
		// ReadWrite rwAnno = method.getAnnotation(ReadWrite.class);
		Transactional trAnno = realMethod.getAnnotation(Transactional.class);
		if (trAnno == null) {
			trAnno = invocation.getThis().getClass().getAnnotation(Transactional.class);
		}

		// 准备分库策略
		RepositoryShardingStrategy repositoryShardingStrategy = null;
		Object obj = null;
		if (rsAnno != null) {
			String strategy = rsAnno.strategy();
			String key = rsAnno.key();
			Object[] args = invocation.getArguments();

			String[] paraNames = paraNameDiscoverer.getParameterNames(realMethod);

			obj = Utils.getSpelValue(args, paraNames, key, applicationContext);

			repositoryShardingStrategy = repositoryShardingStrategyMap.get(strategy);
		} else {
			LOGGER.debug("repository sharding annotation is null {}", debugInfo);
		}

		if (repositoryShardingStrategy == null) {
			repositoryShardingStrategy = NoRepositoryShardingStrategy.getInstance();
		}

		// 分库逻辑处理
		boolean needClean = true;
		if (StrategyHolder.getShardingStrategy() == null) {
			LOGGER.debug("is parent method {}", debugInfo);

			StrategyHolder.setRepositoryShardingKey(repositoryShardingStrategy.getReadWriteDataSource(obj),
					trAnno != null, CleanType.REPOSITORY);
		} else if (StrategyHolder.getRepositoryShardingKey() == null) {
			LOGGER.debug("repository sharding key is null {}", debugInfo);

			StrategyHolder.setRepositoryShardingKey(repositoryShardingStrategy.getReadWriteDataSource(obj),
					trAnno != null, CleanType.REPOSITORY);
		} else if (!StrategyHolder.hasTransaction()) {
			LOGGER.debug("parent method has not transaction {}", debugInfo);

			StrategyHolder.setRepositoryShardingKey(repositoryShardingStrategy.getReadWriteDataSource(obj),
					trAnno != null, CleanType.REPOSITORY);
		} else if (trAnno != null && (Propagation.REQUIRES_NEW.equals(trAnno.propagation())
				|| Propagation.NOT_SUPPORTED.equals(trAnno.propagation()))) {
			LOGGER.debug("nested method marked as REQUIRES_NEW or NOT_SUPPORTED {}", debugInfo);

			StrategyHolder.setRepositoryShardingKey(repositoryShardingStrategy.getReadWriteDataSource(obj), true,
					CleanType.REPOSITORY);
		} else if (!StrategyHolder.getRepositoryShardingKey()
				.equals(repositoryShardingStrategy.getReadWriteDataSource(obj))) {
			throw new IllegalArgumentException(
					"repository sharding key of current method is diffrent with parent method");
		} else {
			needClean = false;
		}

		try {
			return invocation.proceed();
		} catch (Throwable e) {
			throw e;
		} finally {
			if (needClean) {
				StrategyHolder.removeShardingStrategy(CleanType.REPOSITORY);
			}

			LOGGER.debug("get out repository sharding data source interceptor {}", debugInfo);
		}
	}

	@Override
	public void afterPropertiesSet() throws Exception {
	}

	public void setRepositoryShardingStrategies(Map<String, RepositoryShardingStrategy> repositoryShardingStrategyMap) {
		this.repositoryShardingStrategyMap = repositoryShardingStrategyMap;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}
}
